from __future__ import division

################################################################################
#
# convert_date_time.py - Provides date/time conversions as part of the 'convert'
#                        subpackage. Not to be imported by itself!
#
################################################################################

__author__ = 'Adrian Price-Whelan <adrn@nyu.edu>'

# Standard library dependencies
import datetime
import math
import re, sys

dayNames = {0:'Sunday', 1:'Monday', 2:'Tuesday', 3:'Wednesday', 4:'Thursday', 5:'Friday', 6:'Saturday'}

__all__ = ["dec2sex", 
            "sex2dec",
            "hms2stringTime",
            "dec2stringTime",
            "ymd2jd", 
            "utcDatetime2gmst", 
            "gmst2utcDatetime", 
            "mjd2jd", 
            "jd2mjd", 
            "mjd2sdssjd", 
            "jd2sdssjd", 
            "sdssjd2mjd", 
            "sdssjd2jd", 
            "jd2datetime", 
            "mjd2datetime", 
            "datetime2jd", 
            "datetime2mjd", 
            "gmst2lst", 
            "gmstDatetime2lstDatetime", 
            "lst2gmst", 
            "datetime2decimalTime"]

def dec2sex(dec_hours, microsecond=False):
    """ Convert a time in decimal hours to sexagesimal.
    
    Parameters
    ----------
    dec_hours : float
        A time in decimal hours.
    
    microsecond : boolean
        If true, will output a 4-tuple (hour, minute, second, microsecond). If false,
        it will output a 3-tuple with a floating point 'second' term.
    
    Returns
    -------
        See 'microsecond' above.
    
    Notes
    -----
        This is accurate to 1 microsecond.
        
    """
    
    #td = datetime.timedelta(hours=float(dec_hours)
    #hours = td.seconds // 3600
    #remainder = td.seconds - (hours*3600.)
    #minutes = int(remainder // 60)
    #seconds = remainder - (minutes*60.)
    
    (hrRemainder, hours) = math.modf(dec_hours)
    (minRemainder, minutes) = math.modf(hrRemainder*60.0)
    (secRemainder, seconds) = math.modf(minRemainder*60.0)
    microseconds = int(secRemainder*1E6)

    if microsecond:
        return (int(hours), int(minutes), int(seconds), microseconds)
    else:
        return (int(hours), int(minutes), seconds+secRemainder)

def sex2dec(hour, minute, second, microsecond=0.0):
    """ Convert a sexagesimal time to decimal hours.
    
    Parameters
    ----------
    hour, min, sec : int, float
    
    Returns
    -------
        Time in decimal hours
    
    """
    return float(hour) + minute/60.0 + (second+microsecond/1E6)/3600.0

def hms2stringTime(h, m, s, precision=5):
    """ Convert a sexagesimal time to a formatted string.
    
    Parameters
    ----------
    hour, min, sec : int, float
    precision : int
    
    Returns
    -------
        String formatted HH:MM:SS.SSSSS
    
    """
    if h < 0 or m < 0 or s < 0: pm = "-"
    else: pm = "+"
    
    formString = "%s%02d:%02d:%0" + str(precision+3) + "." + str(precision) + "f"
    return formString % (pm, abs(h), abs(m), abs(s))

def dec2stringTime(decim, precision=5):
    """ Convert a decimale time or coordinate to a formatted string.
    
    Parameters
    ----------
    decim : int, float
    precision : int
    
    Returns
    -------
        String formatted HH:MM:SS.SSSSS
    
    """
    return hms2stringTime(*dec2sex(decim), precision=precision)
    
def datetime2decimalTime(datetimeObj=None):
    """ Converts a Python datetime.datetime object into a decimal hour """
    if datetimeObj == None:
        datetimeObj = datetime.datetime.now()
    return sex2dec(datetimeObj.hour, datetimeObj.minute, datetimeObj.second, datetimeObj.microsecond)

def ymd2jd(year, month, day):
    """ 
    Converts a year, month, and day to a Julian Date.
    This function uses an algorithm from the book "Practical Astronomy with your 
    Calculator" by Peter Duffet-Smith (Page 7)
    
    Parameters
    ----------
    year : int
        A Gregorian year
    month : int
        A Gregorian month
    day : int
        A Gregorian day
    
    Returns
    -------
    jd : float
        A Julian Date computed from the input year, month, and day.
    
    """
    if month == 1 or month == 2:
        yprime = year - 1
        mprime = month + 12
    else:
        yprime = year
        mprime = month
    
    if year > 1582 or (year == 1582 and (month >= 10 and day >= 15)):
        A = int(yprime / 100)
        B = 2 - A + int(A/4.0)
    else:
        B = 0   
        
    if yprime < 0:
        C = int((365.25 * yprime) - 0.75)
    else:
        C = int(365.25 * yprime)
    
    D = int(30.6001 * (mprime + 1))
    
    return B + C + D + day + 1720994.5

def utcDatetime2gmst(datetimeObj):
    """ 
    Converts a Python datetime object with UTC time to Greenwich Mean Sidereal Time.
    This function uses an algorithm from the book "Practical Astronomy with your 
    Calculator" by Peter Duffet-Smith (Page 17)
    
    Parameters
    ----------
    datetimeObj : datetime.datetime
        A Python datetime.datetime object
    
    Returns
    -------
    < > : datetime.datetime
        A Python datetime.datetime object corresponding to the Greenwich Mean 
        Sidereal Time of the input datetime.datetime object. 
    
    """
    jd = ymd2jd(datetimeObj.year, datetimeObj.month, datetimeObj.day)
    
    S = jd - 2451545.0
    T = S / 36525.0
    T0 = 6.697374558 + (2400.051336 * T) + (0.000025862 * T**2)
    T0 = T0 % 24
    
    UT = time2decHours(datetimeObj.time()) * 1.002737909
    T0 += UT
    
    GST = T0 % 24

    h,m,s = dec2sex(GST)
    return datetime.datetime(year=datetimeObj.year, month=datetimeObj.month, day=datetimeObj.day, hour=h, minute=m, second=int(s), microsecond=int((s-int(s))*10**6))
    
def gmst2utcDatetime(datetimeObj):
    """ 
    Converts a Python datetime object representing a Greenwich Mean Sidereal Time 
    to UTC time. This function uses an algorithm from the book "Practical Astronomy
    with your Calculator" by Peter Duffet-Smith (Page 18)
    
    Parameters
    ----------
    datetimeObj : datetime.datetime
        A Python datetime.datetime object
    
    Returns
    -------
    < > : datetime.datetime
        A Python datetime.datetime object corresponding to UTC time of the input 
        Greenwich Mean Sidereal Time datetime.datetime object. 
    
    """
    jd = ymd2jd(datetimeObj.year, datetimeObj.month, datetimeObj.day)
    
    S = jd - 2451545.0
    T = S / 36525.0
    T0 = 6.697374558 + (2400.051336*T) + (0.000025862*T**2)
    T0 = T0 % 24
    
    GST = (time2decHours(datetimeObj.time()) - T0) % 24
    UT = GST * 0.9972695663
    
    h,m,s = dec2sex(UT)
    return datetime.datetime(year=datetimeObj.year, month=datetimeObj.month, day=datetimeObj.day, hour=h, minute=m, second=int(s), microsecond=int((s-int(s))*10**6))

def mjd2jd(mjd):
    """ 
    Converts a Modified Julian Date to Julian Date
    
    Parameters
    ----------
    mjd : float (any numeric type)
        A Modified Julian Date
    
    Returns
    -------
    jd : float
        The Julian Date calculated from the input Modified Julian Date
     
    Examples
    --------
    >>> mjd2jd(55580.90429)
    2455581.40429
    
    """
    return mjd + 2400000.5

def jd2mjd(jd):
    """ 
    Converts a Julian Date to a Modified Julian Date
    
    Parameters
    ----------
    jd : float (any numeric type)
        A julian date
    
    Returns
    -------
    mjd : float
        The Modified Julian Date (MJD) calculated from the input Julian Date
     
    Examples
    --------
    >>> jd2mjd(2455581.40429)
    55580.90429
    
    """
    return float(jd - 2400000.5)

def mjd2sdssjd(mjd):
    """ 
    Converts a Modified Julian Date to a SDSS Julian Date, which is
        used at Apache Point Observatory (APO).
    
    Parameters
    ----------
    mjd : float (any numeric type)
        A Modified Julian Date
    
    Returns
    -------
    mjd : float
        The SDSS Julian Date calculated from the input Modified Julian Date
    
    Notes
    -----
    - The SDSS Julian Date is a convenience used at APO to prevent MJD from rolling
        over during a night's observation. 
    
    Examples
    --------
    >>> mjd2sdssjd(55580.90429)
    55581.20429
    
    """
    return mjd + 0.3
    
def jd2sdssjd(jd):
    """ 
    Converts a Julian Date to a SDSS Julian Date, which is
        used at Apache Point Observatory (APO).
    
    Parameters
    ----------
    jd : float (any numeric type)
        A Julian Date
    
    Returns
    -------
    mjd : float
        The SDSS Julian Date calculated from the input Modified Julian Date
    
    Notes
    -----
    - The SDSS Julian Date is a convenience used at APO to prevent MJD from rolling
        over during a night's observation. 
    
    Examples
    --------
    >>> jd2sdssjd(2455581.40429)
    55581.20429
    
    """
    mjd = jd2mjd(jd)
    return mjd2sdssjd(mjd)

def sdssjd2mjd(sdssjd):
    """ 
    Converts a SDSS Julian Date to a Modified Julian Date 
    
    Parameters
    ----------
    sdssjd : float (any numeric type)
        The SDSS Julian Date
    
    Returns
    -------
    mjd : float
        The Modified Julian Date calculated from the input SDSS Julian Date
    
    Notes
    -----
    - The SDSS Julian Date is a convenience used at APO to prevent MJD from rolling
        over during a night's observation. 
    
    Examples
    --------
    >>> sdssjd2mjd(55581.20429)
    55580.90429
    
    """
    return sdssjd - 0.3
    
def sdssjd2jd(sdssjd):
    """ 
    Converts a SDSS Julian Date to a Julian Date 
    
    Parameters
    ----------
    sdssjd : float (any numeric type)
        The SDSS Julian Date
    
    Returns
    -------
    jd : float
        The Julian Date calculated from the input SDSS Julian Date
    
    Notes
    -----
    - The SDSS Julian Date is a convenience used at APO to prevent MJD from rolling
        over during a night's observation. 
    
    Examples
    --------
    >>> sdssjd2jd(55581.20429)
    2455581.40429
    
    """
    mjd = sdssjd - 0.3
    return mjd2jd(mjd)
    
def jd2datetime(fracJD, timezone=0):
    """ 
    Converts a Julian Date to a Python datetime object. The resulting time is in UTC.
    
    Parameters
    ----------
    jd : float (any numeric type)
        A Julian Date
    timezone : int
        An integer representing the timezone to convert to -- defaults to 0.
        i.e. -5 for EST...
    
    Returns
    -------
    < > : datetime.datetime
        A Python datetime.datetime object calculated using an algorithm from the book
        "Practical Astronomy with your Calculator" by Peter Duffet-Smith (Page 8)
    
    Examples
    --------
    >>> jd2datetime(2455581.40429)
    2011-01-19 21:42:10.000010
    
    """
    JDtmp = fracJD + 0.5 + timezone/24.0
    
    B = L = int(JDtmp)
    F = JDtmp - B + 1.0e-8
    
    if L > 2299160:
        A = int((L - 1867216.25)/36524.25);
        B = int(L + 1. + A - A/4.)
    
    C = int(B + 1524)
    D = int((C - 122.1)/365.25)
    E = int(365.25*D)
    G = int((C - E)/30.6001)
    
    day = int(C - E + F - (30.6001 * G))
    
    if G < 13.5: month = int(G-1)
    else: month = int(G-13)
    
    if month < 2.5: year = int(D-4715)
    else: year = int(D-4716)
    
    hour = int(F*24)
    min = int((F*24 - hour)*60)
    sec = ((F*24 - hour)*60 - min)*60 + 0.5
    microsec = (sec - int(sec))*1e5
    
    return datetime.datetime(year, month, day, hour, min, int(sec), int(microsec))
    
def mjd2datetime(mjd):
    """ 
    Converts a Modified Julian Date to a Python datetime object. The resulting time is in UTC.
    
    Parameters
    ----------
    mjd : float (any numeric type)
        A Modified Julian Date
    
    Returns
    -------
    < > : datetime.datetime
        A Python datetime.datetime object calculated using an algorithm from the book
        "Practical Astronomy with your Calculator" by Peter Duffet-Smith (Page 8)
    
    Examples
    --------
    >>> mjd2datetime(55580.90429)
    2011-01-19 21:42:10.000010
    
    """
    jd = mjd2jd(mjd)
    return jd2datetime(jd)

def datetime2jd(datetimeObj):
    """ 
    Converts a Python datetime object to a Julian Date.
    
    Parameters
    ----------
    datetimeObj : datetime.datetime
        A Python datetime.datetime object calculated using an algorithm from the book
        "Practical Astronomy with your Calculator" by Peter Duffet-Smith (Page 7)
    
    Returns
    -------
    < > : float 
        A Julian Date
    
    Examples
    --------
    >>> datetime2jd(datetimeObject)
    2455581.40429
    
    """
    A = ymd2jd(datetimeObj.year, datetimeObj.month, datetimeObj.day)
    B = time2decHours(datetimeObj.time()) / 24.0
    return A + B

def datetime2mjd(datetimeObj):
    """ 
    Converts a Python datetime object to a Modified Julian Date.
    
    Parameters
    ----------
    datetimeObj : datetime.datetime
        A Python datetime.datetime object calculated using an algorithm from the book
        "Practical Astronomy with your Calculator" by Peter Duffet-Smith (Page 7)
    
    Returns
    -------
    < > : float
        A Modified Julian Date
    
    Examples
    --------
    >>> datetime2mjd(datetimeObject)
    55580.90429
    
    """
    jd = datetime2jd(datetimeObj)
    return jd2mjd(jd)

def gmst2lst(longitude, hour, minute=None, second=None, longitudeDirection='W', longitudeUnits='DEGREES'):
    """ 
    Converts Greenwich Mean Sidereal Time to Local Sidereal Time. 
    
    Parameters
    ----------
    longitude : float (any numeric type)
        The longitude of the site to calculate the Local Sidereal Time. Defaults are
        Longitude WEST and units DEGREES, but these can be changed with the optional 
        parameters lonDirection and lonUnits.
    hour : int (or float)
        If an integer, the function will expect a minute and second. If a float, it
        will ignore minute and second and convert from decimal hours to hh:mm:ss.
    minute : int
        Ignored if hour is a float.
    second : int (any numeric type, to include microseconds)
        Ignored if hour is a float.
    lonDirection : string
        Default is longitude WEST, 'W', but you can specify EAST by passing 'E'.
    lonUnits : string
        Default units are 'DEGREES', but this can be switched to radians by passing
        'RADIANS' in this parameter.
    
    Returns
    -------
    hour : int
        The hour of the calculated LST
    minute : int
        The minutes of the calculated LST
    second: float
        The seconds of the calculated LST
    
    Examples
    --------
    >>> gmst2lst(70.3425, hour=14, minute=26, second=18)
    (9, 44, 55.80000000000126)
    >>> gmst2lst(5.055477, hour=14.4383333333333333, longitudeDirection='E', longitudeUnits='RADIANS')
    (9, 44, 55.79892611013463)
    
    """
    if minute != None and second != None:
        hours = sex2dec(hour, minute, second)
    elif minute == None and second == None:
        hours = hour
    else:
        raise AssertionError('minute and second must either be both set, or both unset.')
    
    if longitudeUnits.upper() == 'DEGREES':
        longitudeTime = longitude / 15.0
    elif longitudeUnits.upper() == 'RADIANS':
        longitudeTime = longitude * 180.0 / math.pi / 15.0
    
    if longitudeDirection.upper() == 'W':
        lst = hours - longitudeTime
    elif longitudeDirection.upper() == 'E':
        lst = hours + longitudeTime
    else:
        raise AssertionError('longitudeDirection must be W or E')
    
    lst = lst % 24.0
    
    return dec2sex(lst)

def gmstDatetime2lstDatetime(longitude, gmst, longitudeDirection='W', longitudeUnits='DEGREES'):
    """ 
    Converts Greenwich Mean Sidereal Time to Local Sidereal Time. 
    
    Parameters
    ----------
    longitude : float (any numeric type)
        The longitude of the site to calculate the Local Sidereal Time. Defaults are
        Longitude WEST and units DEGREES, but these can be changed with the optional 
        parameters lonDirection and lonUnits.
    gmst : datetime.datetime
        A Python datetime.datetime object representing the Greenwich Mean Sidereal Time
    lonDirection : string
        Default is longitude WEST, 'W', but you can specify EAST by passing 'E'.
    lonUnits : string
        Default units are 'DEGREES', but this can be switched to radians by passing
        'RADIANS' in this parameter.
    
    Returns
    -------
    lst : datetime.datetime
    
    """
    hours = time2decHours(gmst)
    
    if longitudeUnits.upper() == 'DEGREES':
        longitudeTime = longitude / 15.0
    elif longitudeUnits.upper() == 'RADIANS':
        longitudeTime = longitude * 180.0 / math.pi / 15.0
    
    if longitudeDirection.upper() == 'W':
        lst = hours - longitudeTime
    elif longitudeDirection.upper() == 'E':
        lst = hours + longitudeTime
    else:
        raise AssertionError('longitudeDirection must be W or E')
    
    lst = lst % 24.0
    h,m,s = dec2sex(lst)
    
    return datetime.datetime.combine(gmst.date(), datetime.time(h,m,int(s)))
    
def lst2gmst(longitude, hour, minute=None, second=None, longitudeDirection='W', longitudeUnits='DEGREES'):
    """ 
    Converts Local Sidereal Time to Greenwich Mean Sidereal Time.
    
    Parameters
    ----------
    longitude : float (any numeric type)
        The longitude of the site to calculate the Local Sidereal Time. Defaults are
        Longitude WEST and units DEGREES, but these can be changed with the optional 
        parameters lonDirection and lonUnits.
    hour : int (or float)
        If an integer, the function will expect a minute and second. If a float, it
        will ignore minute and second and convert from decimal hours to hh:mm:ss.
    minute : int
        Ignored if hour is a float.
    second : int (any numeric type, to include microseconds)
        Ignored if hour is a float.
    longitudeDirection : string
        Default is longitude WEST, 'W', but you can specify EAST by passing 'E'.
    longitudeUnits : string
        Default units are 'DEGREES', but this can be switched to radians by passing
        'RADIANS' in this parameter.
    
    Returns
    -------
    hour : int
        The hour of the calculated GMST
    minute : int
        The minutes of the calculated GMST
    second: float
        The seconds of the calculated GMST
    
    Examples
    --------
    >>> lst2gmst(70.3425, hour=14, minute=26, second=18)
    (19, 7, 40.20000000000607)
    >>> lst2gmst(5.055477, hour=14.4383333333333333, longitudeDirection='E', longitudeUnits='RADIANS')
    (19, 7, 40.20107388985991)
    
    """
    if minute != None and second != None:
        hours = sex2dec(hour, minute, second)
    elif minute == None and second == None:
        hours = hour
    else:
        raise AssertionError('minute and second must either be both set, or both unset.')
    
    if longitudeUnits.upper() == 'DEGREES':
        longitudeTime = longitude / 15.0
    elif longitudeUnits.upper() == 'RADIANS':
        longitudeTime = longitude * 180.0 / math.pi / 15.0
    
    if longitudeDirection.upper() == 'W':
        gmst = hours + longitudeTime
    elif longitudeDirection.upper() == 'E':
        gmst = hours - longitudeTime
    else:
        raise AssertionError('longitudeDirection must be W or E')
    
    gmst = gmst % 24.0
    
    return dec2sex(gmst)

if __name__ == '__main__':
    import unittest
    
    class TestConvertDateTime(unittest.TestCase):
        
        def test_dec2sex(self):
            self.assertAlmostEqual(dec2sex(11.0000000), (11, 0, 0), 9)
            self.assertAlmostEqual(dec2sex(11.5000000), (11, 30, 0), 9)
            self.assertAlmostEqual(dec2sex(11.5100000), (11, 30, 36), 9)
            self.assertAlmostEqual(dec2sex(11.5101000, microsecond=True), (11, 30, 36, 360000), 9)
            self.assertAlmostEqual(dec2sex(11.51010001, microsecond=True), (11, 30, 36, 360036), 9)
        
        def test_sex2dec(self):
            self.assertEqual(sex2dec(11, 0, 0), 11.0)
            self.assertEqual(sex2dec(11, 30, 0), 11.5)
            self.assertEqual(sex2dec(11, 30, 36), 11.51)
            self.assertEqual(sex2dec(11, 30, 36, 360000), 11.5101)
            self.assertEqual(sex2dec(11, 30, 36, 360036), 11.51010001)
        
        def test_datetime2decimalTime(self):
            dt = datetime.datetime(2011, 7, 3, hour=11, minute=30, second=0, microsecond=0)
            self.assertEqual(datetime2decimalTime(dt), 11.5)
            
            dt = datetime.datetime(2011, 7, 3, hour=11, minute=30, second=36)
            self.assertEqual(datetime2decimalTime(dt), 11.51)
            
            dt = datetime.datetime(2011, 7, 3, hour=11, minute=30, second=36, microsecond=360000)
            self.assertEqual(datetime2decimalTime(dt), 11.5101)
            
            dt = datetime.datetime(2011, 7, 3, hour=11, minute=30, second=36, microsecond=360036)
            self.assertEqual(datetime2decimalTime(dt), 11.51010001)
        
        # Still need tests for:
        """
        ymd2jd 
        utcDatetime2gmst
        gmst2utcDatetime
        mjd2jd
        jd2mjd
        mjd2sdssjd
        jd2sdssjd
        sdssjd2mjd
        sdssjd2jd 
        jd2datetime
        mjd2datetime
        datetime2jd
        datetime2mjd
        gmst2lst
        gmstDatetime2lstDatetime
        lst2gmst
        datetime2decimalTime"""

    unittest.main()


